/*
 * Package il.ac.biu.cs.grossmm.impl.activeData
 * File SqlMapping.java
 * 
 * This is a part of presence service framework prototype. 
 * See javadoc for more information.
 *  
 * Copyright (C) 2006 Grossmann Mark
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package il.ac.biu.cs.grossmm.impl.activeData;

import il.ac.biu.cs.grossmm.api.OperationFailedException;
import il.ac.biu.cs.grossmm.api.data.NodeType;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Set;

import org.apache.log4j.Logger;

public abstract class SqlMapping {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = Logger.getLogger(SqlMapping.class);

	final Connection connection;

	final String tableName;

	final SqlPropertyMapping[] propertyMaps;

	final SqlInnerMapping[] nodeSetMaps;

	final NodeType nodeType;

	Cache<SqlNodeSet, Set<SqlInnerNode>> subnodesCache;

	Cache<SqlNodeBase, Object[]> valuesCache;

	final int hashCode;

	PreparedStatement selectValuesById;

	PreparedStatement updateVisible;

	PreparedStatement[] updateValue;

	CallableStatement getLastId;

	static class Counter {
		private int k = 0;

		int increment() {
			return k++;
		}
	}

	SqlMapping(Connection connection, String tableNameBase, NodeType nodeType)
			throws SQLException {
		this(connection, tableNameBase, nodeType, new Counter());
	}

	SqlMapping(Connection connection, String tableNameBase, NodeType nodeType,
			Counter counter) throws SQLException {
		this.connection = connection;
		this.tableName = tableNameBase + counter.increment();
		this.nodeType = nodeType;

		propertyMaps = new SqlPropertyMapping[nodeType.propertiesNumber()];
		nodeSetMaps = new SqlInnerMapping[nodeType.nodesetsNumber()];

		for (int i = 0; i < propertyMaps.length; i++) {
			propertyMaps[i] = SqlPropertyMapping.create(nodeType.property(i));
		}

		for (int i = 0; i < nodeSetMaps.length; i++) {
			NodeType nestedNodeType = nodeType.nodeset(i).nestedNodeType();
			nodeSetMaps[i] = new SqlInnerMapping(this, connection,
					tableNameBase, nestedNodeType, i, counter);
		}

		this.hashCode = nodeType.hashCode() + tableName.hashCode();
	}

	void prepare(Cache<SqlNodeBase, Object[]> valuesCache,
			Cache<SqlNodeSet, Set<SqlInnerNode>> subnodesCache)
			throws SQLException {
		int numValues = propertyMaps.length;

		this.subnodesCache = subnodesCache;
		this.valuesCache = valuesCache;

		// //////////// selectAllById
		// SELECT val_1, ..., val_n FROM <table> WHERE id=?

		StringBuffer sb = new StringBuffer();

		sb.append("SELECT ");

		for (int i = 0; i < numValues; i++) {
			if (i == 0)
				sb.append("val_0");
			else
				sb.append(", val_").append(i);
		}

		sb.append(" FROM ").append(tableName);
		sb.append(" WHERE id=?");

		selectValuesById = connection.prepareStatement(sb.toString());

		// /////////// updateValue
		// UPDATE <table> SET val_k=?
		// WHERE id=?

		updateValue = new PreparedStatement[numValues];
		for (int i = 0; i < numValues; i++) {
			sb.setLength(0);
			sb.append("UPDATE ").append(tableName);
			sb.append(" SET val_").append(i);
			sb.append("=? WHERE id=?");

			updateValue[i] = connection.prepareStatement(sb.toString());
		}

		// /////////// updateVisible
		// UPDATE <table> SET isVisible=true WHERE id=?

		sb.setLength(0);
		sb.append("UPDATE ").append(tableName);
		sb.append(" SET is_visible=true WHERE id=?");
		updateVisible = connection.prepareStatement(sb.toString());

		getLastId = connection.prepareCall("CALL IDENTITY()");

		update("DELETE FROM " + tableName + " WHERE NOT is_visible");
		commit();
	}

	/**
	 * Executes sql update command
	 * 
	 * @param command
	 *            sql update command to execute
	 * @throws SQLException
	 */
	synchronized void update(String command) throws SQLException {
		if (logger.isDebugEnabled()) {
			logger.debug("update() - start");
			logger.debug("SQL = [" + command + "]");
		}

		Statement st = null;

		st = connection.createStatement(); // statements

		int i = st.executeUpdate(command); // run the query

		if (i == -1) {
			throw new SQLException("Unknown Error (executeUpadte returned -1)");
		}

		st.close();

		if (logger.isDebugEnabled()) {
			logger.debug("update() - end");
		}
	}

	/**
	 * @param sb
	 * @param typeMaps
	 */
	protected static void insertFieldDefinitions(StringBuffer sb,
			SqlPropertyMapping[] typeMaps) {
		for (int j = 0; j < typeMaps.length; j++) {
			sb.append(", ");
			sb.append("val_").append(j);
			sb.append(' ').append(typeMaps[j].getSqlType());
			sb.append(" DEFAULT NULL ");
		}

		boolean seenUnique = false;

		for (int j = 0; j < typeMaps.length; j++) {
			if (!typeMaps[j].isUnique())
				continue;

			if (!seenUnique) {
				sb.append(", UNIQUE (");
				sb.append("val_").append(j);
				seenUnique = true;
			} else
				sb.append(", val_").append(j);
		}

		if (seenUnique)
			sb.append(") ");
	}

	public SqlInnerMapping getSubMapping(int index) {
		return nodeSetMaps[index];
	}

	@Override
	public int hashCode() {
		return hashCode;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj != null && obj instanceof SqlInnerMapping) {
			SqlInnerMapping m = (SqlInnerMapping) obj;

			return m.nodeType.equals(nodeType);
			// && m.tableName.equals(tableName);
		} else
			return false;
	}

	static byte[] objectTyBytes(Object o) throws IOException {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream out = new ObjectOutputStream(bos);
		out.writeObject(o);
		out.close();

		return bos.toByteArray();
	}

	static Object bytesToObject(byte[] bytes) throws IOException,
			ClassNotFoundException {
		ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
		ObjectInputStream in = new ObjectInputStream(bis);

		Object o = in.readObject();
		in.close();

		return o;
	}

	public Set<SqlInnerNode> getSubnodes(SqlNodeBase node, int index)
			throws OperationFailedException {
		return subnodesCache.get(new SqlNodeSet(node, index));
	}

	public Set<SqlInnerNode> getSubnodesNocache(SqlNodeBase parent, int index)
			throws SQLException {
		SqlInnerMapping subMapping = nodeSetMaps[index];
		return subMapping.childrenOf(parent);
	}

	public Object[] getValues(SqlNodeBase node) throws OperationFailedException {
		return valuesCache.get(node);
	}

	public Object[] read(SqlNodeBase node) throws Exception {
		selectValuesById.setInt(1, node.id);
		ResultSet rs = selectValuesById.executeQuery();

		if (!rs.next())
			return null;

		int numValues = propertyMaps.length;

		Object[] values = new Object[numValues];

		for (int i = 0; i < values.length; i++) {
			// byte[] bytes = rs.getBytes(i + 1);
			// values[i] = bytesToObject(bytes);

			values[i] = propertyMaps[i].get(rs, i + 1);
		}

		return values;
	}

	public void write(SqlNodeBase node, Object[] oldValues, Object[] newValues)
			throws Exception {
		int numValues = propertyMaps.length;
		for (int i = 0; i < numValues; i++) {
			if (oldValues[i] == newValues[i])
				continue;

			// byte[] bytes = objectTyBytes(newValues[i]);

			propertyMaps[i].set(updateValue[i], 1, newValues[i]);
			updateValue[i].setInt(2, node.id);
			updateValue[i].executeUpdate();
		}

		valuesCache.update(node, newValues);
	}

	public void makeVisible(SqlNodeBase node) throws SQLException {
		updateVisible.setInt(1, node.id);
		updateVisible.executeUpdate();
	}

	public void commit() throws SQLException {
		connection.commit();
	}

}
